配合分支:day16/start,Repo: 按我
Day 15 修正讀取照片錯誤,引入了 sealed class 來表示:成功、失敗。本章要來說明,有很多方式可以修正問題,有些時候,並不一定有 標準答案。(當然嚴重的資安、隱私⋯ 等,問題則另當別論)。昨天的案例可以來說明各種修正的方式,以及 為什麼要選擇 Sealed Class 來修正,這牽扯到每一個 class 的責任歸屬。
今天來新增一個情境:判斷照片「不存在」。(昨天是每一個錯誤,都丟一樣的訊息,對這個情境做擴充)。驗證方式是隨意將一個照片在 App 儲存空間改名(可以透過 Android Studio 的 Device Explorer 來辦到)
大概分為幾個層面來做修正:
OwnedPhotoImage 碰到 Unavailable 時,順便檢查檔案是否存在?PhotoReadResult 代表「檔案不存在」三者皆可以修正問題, 說明的方向會是 1 -> 2。先講結論,最後會選擇 2 來進行調整。待我一一說明如下。
OwnedPhotoImage 碰到 Unavailable 時,順便檢查檔案是否存在?Unavailable就已經是錯誤,直接在這邊判斷就好囉。的確可以這麼做沒有錯,程式碼大概會長成這個樣子:

遇到問題,去問 fileName 去問檔案是否存在,不存在就將顯示的訊息變成:
照片暫時無法讀取。
雖然放在這,的確修正了問題,回到本章要提的「權責」。OwnedPhotoImage 是一個 ComposeView (又或者,稱為 Composable 或 Composable View)。名稱裡頭有 View 就代表這個程式碼最好只負責「純顯示畫面」。
不過在這裡他做了其他的工作:去問系統檔案存不存在?這很顯然的已經超過了純顯示畫面的範疇。
context.filesDir.resolve(fileName).exists()
第二點是,在這裡多了一個 if 判斷式。如果未來有更多的問題需要被檢查,例如「這個照片副檔名支不支援?」例如使用者傳了一個 Raw 相片格式之類的?
除了判斷式越來越複雜,變得不好讀以外。(未來可能進化成 when 判斷式⋯ 🥤 )代表未來測試到顯示錯誤照片邏輯這一塊,又多了一個變數/路徑需要測試。
顯然未來這邊變數越加越多,走到 Unavailable 會越來越複雜,也多了壞掉的可能。
依然是一個比較好的處理方式。怎麼說?先來產生一個新的 PhotoReadResult 種類—— MissingFile。
class MissingFile : PhotoReadResult()
雖然牽一髮而動全身,不過在「資料流」上會單純很多。同時,權責方便也會比較清晰,待我分解。
open() 時多了一個錯誤要處理:

從程式碼來看,剛好配合 inputStream 出現錯誤時,會拋出的 Exception。這裡回新增的 MissingFile 錯誤。
而這個 open() ,目前只有在 OwnedPhotoImage 這個「畫面」使用。
要擴充也十分的單純,只要 when 表達式補上即可。
is PhotoReadResult.MissingFile -> {
Box(
modifier = modifier.clipToBounds(),
contentAlignment = Alignment.Center
) {
Text(text = "照片檔案已遺失")
}
}
容易理解,以後要改也很簡單。
為何這類的「讀取檔案」的問題,放在 LocalPhotoStore 比較好呢?這個 Class 的責任是去取得、寫入照片到 App 儲存空間裡面,取得照片本來就是他的職責。
但是 OwnedPhotoImage 的職責是「呈現目前的狀態到畫面上」。單純由自己去判斷是否有照片,有點逾越了他的職權。(抽象的來看啦)
從資料流來看也會比較清晰,ComposeView 等「View(中文應該翻譯成:視圖,但大家都講 View 啦)」從其他人那邊取得狀態後畫上去。而 LocalPhotoStore 負責取資料,給資料。
理想上資料流:

不過這並非最理想的使用方式,讀者可能發現明明 OwnedPhotoImage只是一個 View 怎麼可以自己生一個 LocalPhotoStore?欸,終於發現了這個問題了嗎?這會造成上面資料流的圖片有點對不起來。這一點,請待之後的課程再依序幫各位介紹。
MissingFile 把「檔案不存在」獨立成專屬的錯誤結果。OwnedPhotoImage 應該只管畫出畫面,不該越權去拿資料。LocalPhotoStore 提供狀態給 View 顯示,讓資料傳遞單純好懂。